#! /usr/bin/env python3.2

# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Apologies for the lack of comments.
#
# Arguments that are supposed to refer to a particular element in a
# doxygen xml file are named the same as that element.
#
# I'm trying to assert-fail when something unexpected happens in the
# XML file rather than recovering so that I don't silently lose text.

from copy import deepcopy
from lxml import etree
from lxml.builder import E
import datetime
import logging
import os
import sys

logging.basicConfig(level=logging.DEBUG)

NBSP = '\u00A0'

_REF_CACHE={}
def ref(element):
    """Expects element.@refid to point to another xml file.
    Returns that file's root element."""
    refid = element.attrib['refid']
    if refid not in _REF_CACHE:
        base_url = os.path.dirname(element.getroottree().docinfo.URL)
        _REF_CACHE[refid] = etree.parse(os.path.join(base_url, refid + '.xml'))
    return _REF_CACHE[refid]

def only(list):
    assert len(list) == 1, 'Expected exactly one element in ' + str(list)
    return list[0]

def justtext(node):
    """Asserts that 'node' contains just a text string without other
    markup, and returns that string."""
    assert isinstance(node, etree._Element), (type(node), getattr(node, '__mro__', None))
    assert len(node) == 0, (
        'Expected node to contain only text, but was: ' +
        str(etree.tostring(node)))
    return str(node.text)

def isspace(text):
    """True if 'text' is empty or whitespace"""
    return text is None or len(text) == 0 or text.isspace()

def empty(node):
    """True if node contains only whitespace."""
    return len(node) == 0 and isspace(node.text)

def strip_prefix(prefix, string):
    assert string.startswith(prefix), ('Expected %s to start with %s' %
                                       (string, prefix))
    return string[len(prefix):]

class TextNode:
    def __init__(self, node):
        self.node = node
    def __str__(self): raise NotImplementedError
    def set(self, newtext): raise NotImplementedError
class NodeText(TextNode):
    def __str__(self):
        return str(self.node.text or '')
    def set(self, newtext):
        self.node.text = newtext
    def append(self, newtext):
        self.node.text = (self.node.text or '') + newtext
class NodeTail(TextNode):
    def __str__(self):
        return str(self.node.tail or '')
    def set(self, newtext):
        self.node.tail = newtext
    def append(self, newtext):
        self.node.tail = (self.node.tail or '') + newtext

def flatten_node(node):
    """Replaces node with its contents."""
    assert len(node.attrib) == 0, (
        'Would lose attribute information from %s' % etree.tostring(node))

    def append_previous_text(new_text):
        """Finds the text previous to 'node', and appends new_text to it."""
        if new_text is None:
            return
        if node.getprevious() is not None:
            NodeTail(node.getprevious()).append(new_text)
        else:
            NodeText(node.getparent()).append(new_text)

    append_previous_text(node.text)
    for child in node:
        node.addprevious(child)
    append_previous_text(node.tail)
    node.getparent().remove(node)

def iter_nodes_and_text(node, with_tail=True):
    yield node
    if node.text is not None:
        yield NodeText(node)
    for child in node:
        for result in iter_nodes_and_text(child):
            yield result
    if with_tail and node.tail is not None:
        yield NodeTail(node)

def count_initial_spaces(children):
    """Counts the smallest number of spaces that begins a line in children."""
    min_initial_spaces = sys.maxsize
    initial_spaces = 0

    def update_from_text(text):
        nonlocal min_initial_spaces, initial_spaces
        stripped = text.lstrip()
        # Count the &nbsp;s since they're all that'll indent the line.
        initial_spaces += text[:len(text)-len(stripped)].count(NBSP)
        if len(stripped) > 0:
            # Found first non-whitespace character; update
            # min_initial_spaces.
            if initial_spaces < min_initial_spaces:
                min_initial_spaces = initial_spaces
            initial_spaces = -1

    for child in children:
        # child is either an Element or a str.
        if isinstance(child, str):
            update_from_text(child)
            continue
        for node in iter_nodes_and_text(child):
            # node is an Element or a text node.
            if getattr(node, 'tag', None) == 'br':
                # If this was an all-whitespace line, it was
                # "blank", so shouldn't affect the initial-space
                # count.
                initial_spaces = 0
            elif initial_spaces == -1:
                continue
            elif isinstance(node, TextNode):
                update_from_text(str(node))
    return min_initial_spaces

def remove_initial_characters(children, num_chars):
    """Removes num_chars from the beginning of each line in children."""
    if num_chars == 0:
        return
    num_this_line = num_chars

    def remove_from_text(text):
        nonlocal num_this_line
        split_point = 0
        while (split_point < len(text)
               and num_this_line > 0
               and text[split_point].isspace()):
            if text[split_point] == NBSP:
                # Ignore non-&nbsp; spaces.
                num_this_line -= 1
            split_point += 1
        return text[split_point:]

    for index, child in enumerate(children):
        # child is either an Element or a str.
        if isinstance(child, str):
            children[index] = remove_from_text(child)
            continue
        for node in iter_nodes_and_text(child):
            # node is an Element or a text node.
            if getattr(node, 'tag', None) == 'br':
                num_this_line = num_chars
            elif num_this_line == 0:
                continue
            elif isinstance(node, TextNode):
                node.set(remove_from_text(str(node)))

def render_children(node, result, child_renderer):
    if node.text:
        result.append(str(node.text))
    for child in node:
        child_renderer(child, result)
        if child.tail:
            result.append(str(child.tail))
    return result

def constant(f):
    """Use as a decorator to define a constant with the result of a function."""
    return f()

@constant
def PHRASING_RENDERERS():
    def ref(node, result):
        result.append(E.a(dict(href='#' + node.attrib['refid']),
                          *render_children(node, [], render_phrasing)))
    def sp(node, result):
        result.append(NBSP)
    def linebreak(node, result):
        result.append(E.br())
    def highlight(node, result):
        result.append(E.span({'class': node.attrib['class']},
                             *render_children(node, [], render_phrasing)))
    def anchor(node, result):
        assert empty(node), 'Unexpected anchor contents: %s' % etree.tostring(node)
        result.append(E.a(dict(id=node.attrib['id'])))
    def ulink(node, result):
        result.append(E.a(dict(href=node.attrib['url']),
                          *render_children(node, [], render_phrasing)))
    def computeroutput(node, result):
        result.append(E.code(*render_children(node, [], render_phrasing)))
    def emphasis(node, result):
        result.append(E.em(*render_children(node, [], render_phrasing)))
    return locals()

def render_phrasing(node, result=None):
    """Transforms phrasing-level doxygen markup inside node to HTML.

    The name "phrasing" comes from
    http://developers.whatwg.org/content-models.html#phrasing-content.

    Returns a list of mixed text and HTML nodes.  If a list is passed
    in as 'result', that list will be extended and returned.
    """
    if result is None:
        result = []

    if node.tag in PHRASING_RENDERERS:
        PHRASING_RENDERERS[node.tag](node, result)
    else:
        render_children(node, result, render_phrasing)
    return result

@constant
def FLOW_RENDERERS():
    def para(node, result):
        if node.xpath('document-number'):
            # The paragraph containing the document number is metadata
            # that's already been used by the top-level body renderer.
            return
        elif (isspace(node.text)
            and len(node) == 1
            and node[0].tag in FLOW_RENDERERS):
            # If there's text after the contained node, it winds up
            # outside of any paragraph, which I think is better than
            # starting an explicit new paragraph before it, since
            # doxygen didn't think it deserved its own paragraph.
            render_children(node, result, render_flow)
        elif (node.getparent() is not None
              and node.getparent().tag in ('listitem',)
              and len(node.getparent()) == 1
              and isspace(node.getparent().text)
              and isspace(node.tail)):
            # Skip paras that are alone in their parent, since that
            # just adds useless vertical space.
            render_children(node, result, render_flow)
        else:
            children = render_children(node, [], render_flow)
            if any(not getattr(child, 'isspace', lambda:False)()
                   for child in children):
                # Omit empty <p>s.
                result.append(E.p(*children))
    def programlisting(node, result):
        # Doxygen program listings are sometimes indented. Strip the
        # indentation so we can get a consistent appearance in the
        # paper.
        children = render_children(node, [], render_flow)
        remove_initial_characters(children, count_initial_spaces(children))
        result.append(E.div(E.div({'class': 'example'}, *children)))
    def codeline(node, result):
        result.append(E.code(*render_children(node, [], render_phrasing)))
        result.append(E.br())
    def preformatted(node, result):
        result.append(E.pre(*render_children(node, [], render_phrasing)))

    def sect1(node, result):
        result.append(E.section(*render_children(node, [], render_flow)))
    def title(node, result):
        section = node.getparent()
        if section.tag == 'simplesect' and section.attrib['kind'] == 'par':
            # The title has already been rendered.
            return
        hN = dict(sect1=E.h2, sect2=E.h3, sect3=E.h4)[section.tag]
        result.append(E.header(hN(E.a(dict(name=section.attrib['id']),
                                      justtext(node)))))

    def xrefsect(node, result):
        result.append(E.div(E.figure(
                    dict(id=node.attrib['id']),
                    E.figcaption(justtext(uniquechild(node, 'xreftitle'))),
                    *render_children(uniquechild(node, 'xrefdescription'), [], render_flow)
                    )))
    def simplesect(node, result):
        kind = node.attrib['kind']
        if kind == 'see':
            # I'm using a \sa on the main page to order class
            # documentation, so scan in here for <ref> elements and
            # include them directly.
            for ref_node in node.iter('ref'):
                assert ref_node.attrib['kindref'] == 'compound', (
                    'Unexpected kindref: ' + ref_node.attrib['kindref'])
                result.extend(render_compound_file(ref(ref_node)))
        elif kind in ('return', 'pre', 'post'):
            result.append(E.dl({'class': 'attribute'},
                               E.dt({'return': 'Returns',
                                     'pre': 'Requires',
                                     'post': 'Postconditions'}[kind] +':'),
                               E.dd(*render_children(node, [], render_flow))))
        elif kind == 'par':
            # 'par' usually just marks a paragraph, and contains child
            # <para> nodes, so just render them.  However, if it has a
            # title, then it's a function attribute like return, pre,
            # or post.
            title = maybechild(node, 'title')
            if title is not None and not empty(title):
                result.append(E.dl({'class': 'attribute'},
                                   E.dt(justtext(title)),
                                   E.dd(*render_children(node, [], render_flow))))
            else:
                result.extend(render_children(node, [], render_flow))
        elif kind == 'note':
            result.append(E.div(E.div({'class': 'note'},
                                      *render_children(node, [], render_flow))))
        elif kind == 'author':
            result.append(E.address(*render_children(node, [], render_flow)))
        else:
            raise ValueError('Unexpected kind %s in simplesect' % kind)

    def variablelist(node, result):
        dl_items = []
        for item in node:
            if item.tag == 'varlistentry':
                term = uniquechild(item, 'term')
                dl_items.append(E.dt(*render_children(term, [], render_phrasing)))
            elif item.tag == 'listitem':
                dl_items.append(E.dd(*render_children(item, [], render_flow)))
        todo_class = {}
        for compounddef in node.iterancestors(tag='compounddef'):
            if compounddef.attrib['id'] == 'todo':
                todo_class['class'] = 'todo'
        result.append(E.dl(todo_class, *dl_items))

    def orderedlist(node, result):
        result.append(E.ol(*render_children(node, [], render_flow)))
    def itemizedlist(node, result):
        result.append(E.ul(*render_children(node, [], render_flow)))
    def listitem(node, result):
        result.append(E.li(*render_children(node, [], render_flow)))

    return locals()

def render_flow(node, result=None):
    """Transforms flow-level doxygen markup inside node to HTML.

    The name "flow" comes from
    http://developers.whatwg.org/content-models.html#flow-content.

    Returns a list of mixed text and HTML nodes.  If a list is passed
    in as 'result', that list will be extended and returned.
    """
    if result is None:
        result = []

    if node.tag in FLOW_RENDERERS:
        FLOW_RENDERERS[node.tag](node, result)
    elif node.tag in PHRASING_RENDERERS:
        PHRASING_RENDERERS[node.tag](node, result)
    else:
        render_children(node, result, render_flow)
    return result

def maybechild(node, xpath):
    """Returns the child of node referred to by 'xpath'.
    Asserts that there is 0 or 1 such child."""
    results = node.xpath(xpath)
    assert len(results) <= 1, ('More than one result of %s in %s' %
                               (childtag, etree.tostring(node)))
    if len(results) == 0:
        return None
    assert results[0].getparent() == node, (
        'Result %s wasn\'t a child of node %s' %
        (etree.tostring(results[0])[:200], etree.tostring(node)[:200]))
    return results[0]

def uniquechild(node, xpath):
    """Returns the child of node referred to by 'xpath'.
    Asserts that this child is unique."""
    result = maybechild(node, xpath)
    assert result is not None, ('%s has no results in %s' %
                                (xpath, etree.tostring(node)[:200]))
    return result

def render_compound_file(compound_file):
    """Returns a list of mixed text and nodes."""
    root = compound_file.getroot()
    assert root.tag == 'doxygen', 'File had unexpected root: ' + root.tag
    compounddef = uniquechild(root, 'compounddef')
    kind = compounddef.attrib['kind']
    if kind in ('class', 'struct'):
        return document_class(compounddef)
    if kind == 'page':
        return render_page(compounddef)
    raise ValueError('Unexpected kind %s in compounddef from %s' %
                     (kind, compound_file.docinfo.URL))

def render_page(compounddef):
    id = compounddef.attrib['id']
    if id == 'todo':
        return ([E.header(E.h2('Open questions'))] +
                render_flow(uniquechild(compounddef, 'detaileddescription')))
    else:
        raise ValueError('Unexpected page: %s' % id)

def render_declaration(class_file, indent):
    compounddef = only(class_file.xpath('compounddef'))
    return ([indent] +
            template_decl(compounddef.xpath('templateparamlist'), indent=indent) +
            [ compounddef.attrib['kind'],
              ' ',
              compounddef.xpath('substring-after(compoundname, "std::")'),
              '\n'])

def find_trailing_return_type(memberdef):
    """Doxygen doesn't understand trailing return types, so try to
    pull it out of the argsstring."""
    argsstring = justtext(uniquechild(memberdef, 'argsstring'))
    assert argsstring[0] == '(', '"%s" didn\'t start with open paren' % argsstring
    i = 1
    parens = 1
    while parens > 0 and i < len(argsstring):
        if argsstring[i] == '(':
            parens += 1
        elif argsstring[i] == ')':
            parens -= 1
        i += 1
    assert parens == 0, 'Mismatched parens in "%s"' % argsstring
    # Try to find a '->' after the close paren.
    arrow_index = argsstring.find('->', i)
    if arrow_index == -1:
        return ''
    # Return everything after it.
    return ' ' + argsstring[arrow_index:]

def render_args(memberdef):
    args = ['(']
    for param in memberdef.iterchildren(tag='param'):
        type_array = render_phrasing(uniquechild(param, 'type'))
        declname = maybechild(param, 'declname')
        defval = maybechild(param, 'defval')
        if defval is not None and any('enable_if' in type_bit
                                      for type_bit in type_array):
            # enable_if arguments aren't intended for the user to type
            # in, and they're too verbose to include in the spec.
            break
        if maybechild(param, 'array') is not None:
          # TODO: Array parameters are formatted in a way that's hard
          # to implement here. For now, just punt back to the literal
          # argsstring. If there's ever an enable_if with an array,
          # I'll have to fix this.
          return [justtext(uniquechild(memberdef, 'argsstring'))]
        args.extend(type_array)
        if declname is not None or defval is not None:
            args.append(' ')
        if declname is not None:
            args.extend(render_phrasing(declname))
        if defval is not None:
            args.append('=')
            args.extend(render_phrasing(defval))
        args.append(', ')
    if args[-1] == ', ':
        args.pop()  # Remove the final comma.
    args.append(')')
    if memberdef.attrib['const'] == 'yes':
        args.append(' const')
    args.append(find_trailing_return_type(memberdef))
    return args

def render_base_list(compounddef):
    bases = compounddef.xpath('basecompoundref')
    if not bases:
        return []
    prev_protection = {'class': 'private', 'struct': 'public'}[
        compounddef.attrib['kind']]
    result = []
    for i, basecompoundref in enumerate(bases):
        if i == 0:
            result.append(' : ')
        else:
            result.append(', ')
        if basecompoundref.attrib['prot'] != prev_protection:
            result.extend((basecompoundref.attrib['prot'], ' '))
        if basecompoundref.attrib['virt'] == 'virtual':
            result.append('virtual ')
        result.append(basecompoundref.text)
    return result

def document_class(compounddef):
    class_decl = (
        ['namespace std {\n  '] +
        template_decl(compounddef.xpath('templateparamlist'), indent='  ') +
        [compounddef.attrib['kind'],
         ' ',
         strip_prefix('std::',
                      justtext(uniquechild(compounddef, 'compoundname')))
         ])
    class_decl.extend(render_base_list(compounddef))
    class_decl.append(' {\n  public:\n')
    class_decl.extend(declare_members(compounddef, indent='    ', nonmembers=False))
    class_decl.append('  };\n')
    class_decl.extend(declare_members(compounddef, indent='  ', nonmembers=True))
    class_decl.append('}')
    class_decl = E.pre(E.code(*class_decl))
    return E.section(
        *([E.header(E.h2(E.a(dict(name=compounddef.attrib['id']),
                             justtext(uniquechild(compounddef, 'compoundname')))))] +
          render_flow(uniquechild(compounddef, 'briefdescription')) +
          render_flow(uniquechild(compounddef, 'detaileddescription')) +
          [E.h3('Class definition'),
           class_decl,
           E.section(
                    E.header(E.h3('Member semantics')),
                    *define_members(compounddef)
                    )]
          )
        )

def declare_members(compounddef, indent, nonmembers):
    """Returns a list of section and member declarations.

    Args:
      indent: str containing spaces to insert at the beginning of each line.
      nonmembers: True if non-member but associated functions should be printed.
    """
    if nonmembers:
        def include(member_or_section):
            return bool(member_or_section.xpath('.//nonmember'))
    else:
        def include(member_or_section):
            return not member_or_section.xpath('.//nonmember')
    result = []
    first = True;
    for child in compounddef:
        if not include(child):
            continue
        if child.tag == 'sectiondef':
            if child.attrib['kind'].startswith('private-'):
                continue
            if not first or nonmembers:
                result.append('\n')
            members = [indent + '// ',
                       justtext(uniquechild(child, 'header')),
                       '\n']
            for member in child:
                if member.tag == 'memberdef':
                    members.extend(declare_member(member, indent))
            result.extend(members)
        elif child.tag == 'memberdef':
            result.extend(declare_member(child))
        else:
            continue
        first = False
    return result

def declare_member(memberdef, indent):
    result = [indent]
    kind = memberdef.attrib['kind']
    if kind == 'function':
        name = justtext(uniquechild(memberdef, 'name'))
        if name == 'static_assert':
            # Doxygen doesn't realize that static_assert is a new
            # C++11 keyword, so skip it here.
            return []
        result.extend(template_decl(memberdef.xpath('templateparamlist'),
                                    indent=indent))
        if memberdef.attrib['static'] == 'yes':
            result.append('static ')
        if memberdef.attrib['explicit'] == 'yes':
            result.append('explicit ')
        if memberdef.attrib['virt'] == 'virtual':
            result.append('virtual ')
        return_type = render_phrasing(uniquechild(memberdef, 'type'))
        if return_type:
            result.extend(return_type)
            result.append(' ')
        result.append(E.a(dict(href='#' + memberdef.attrib['id']),
                          name))
        result.extend(render_args(memberdef))
    elif kind == 'typedef':
        result.append('typedef ')
        if memberdef.xpath('.//implementation-defined-type'):
          result.append(E.var('implementation_defined'))
        else:
          result.extend(render_phrasing(uniquechild(memberdef, 'type')))
        result.append(' ')
        result.extend(render_phrasing(uniquechild(memberdef, 'name')))
    elif kind == 'variable':
        if memberdef.attrib['static'] == 'yes':
            result.append('static ')
        if memberdef.attrib['mutable'] == 'yes':
            result.append('mutable ')
        result.extend(render_phrasing(uniquechild(memberdef, 'type')))
        result.append(' ')
        result.extend(render_phrasing(uniquechild(memberdef, 'name')))
        initializer = render_phrasing(uniquechild(memberdef, 'initializer'))
        if initializer:
            result.append(' =')
            result.extend(initializer)
    else:
        raise ValueError('Unexpected memberdef kind: %s' % kind)

    result.append(';\n')
    return result

def define_members(compounddef):
    result = []
    for child in compounddef:
        if child.tag == 'sectiondef':
            sectiondef = child
            if sectiondef.attrib['kind'].startswith('private-'):
                continue
            description = sectiondef.xpath('description')
            assert len(description) <= 1, (
                'Expected at most 1 description in %s' % etree.tostring(sectiondef))
            members = []
            if description:
                    members.extend(render_flow(description[0]))
            for member in sectiondef:
                if member.tag == 'memberdef':
                    members.extend(define_described_member(member))
            if members:
                result.append(
                    E.section(E.header(E.h4(justtext(uniquechild(sectiondef, 'header')))),
                              *members))
        elif child.tag == 'memberdef':
            result.extend(define_described_member(child))
        else:
            continue
    return result

def define_described_member(memberdef):
    """Returns the description of memberdef if it has one."""
    briefdescription = uniquechild(memberdef, 'briefdescription')
    detaileddescription = uniquechild(memberdef, 'detaileddescription')
    # Ignore inbodydescription.
    if empty(briefdescription) and empty(detaileddescription):
        return []

    result = [E.header(E.code(dict(id=memberdef.attrib['id']),
                              *declare_member(memberdef, indent='')))]
    result.extend(render_flow(briefdescription))
    result.extend(render_flow(detaileddescription))
    return [E.section({'class': 'function'}, *result)]

def template_decl(templateparamlists, indent):
    if not templateparamlists:
        return []
    templateparamlist = only(templateparamlists)
    result = 'template<'
    for position, param in enumerate(templateparamlist.xpath('param')):
        if position > 0:
            result += ', '
        result += justtext(uniquechild(param, 'type'))
        if param.xpath('declname'):
            result += ' ' + justtext(uniquechild(param, 'declname'))
    result += '>\n' + indent
    return [result]

def remove_broken_links(html):
    for link in html.xpath('//a[@href]'):
        href = link.attrib['href']
        if not href.startswith('#'):
            continue
        id = strip_prefix('#', href)
        targets = html.xpath('//*[@id="%s" or @name="%s"]' % (id, id))
        if not targets:
            del link.attrib['href']
            if not link.attrib:
                # Remove the <a> entirely.
                flatten_node(link)

def merge_adjacent_dls(html):
    """Take sequences of <dl> nodes and merge their contents into the first one.

    We create <dl>s directly from several kinds of simplesect, but
    we really want several <dt>s in the same <dl>.
    """
    for dl in html.xpath('//dl'):
        if dl.getparent() is None:
            # This dl was already merged into a previous dl and detached.
            continue
        while True:
            if not isspace(dl.tail):
                # Next element isn't immediately after this one.
                break
            next_dl = dl.getnext()
            if next_dl is None or next_dl.tag != 'dl':
                break
            for child in next_dl:
                dl.append(child)
            if next_dl.tail:
                dl.tail = next_dl.tail
            dl.getparent().remove(next_dl)

def main(argv):
    base_file = argv[1]
    indexpage = uniquechild(etree.parse(base_file).getroot(), 'compounddef')
    title = justtext(uniquechild(indexpage, 'title'))
    result = E.html(
        E.head(
            E.meta({'http-equiv': 'Content-Type',
                    'content': 'text/html;charset=US-ASCII'}),
            E.title(title),
            E.style(dict(type='text/css'), """
body {color: #000000; background-color: #FFFFFF;}

del {text-decoration: line-through; color: #8B0040;}
ins {text-decoration: underline; color: #005100;}

pre > code:only-child {display: inline-block; white-space: inherit}
code {white-space: nowrap}
code wbr {white-space: normal}

section.function {}
.attribute {margin-left: 2em}
.attribute dt {float: left; font-style: italic; padding-right: 1ex}
.attribute dd {margin-left: 0}
.attribute li {clear: left}

.docinfo {float: right}
.docinfo p {margin: 0; text-align:right; font-style: italic}

section {padding-left: 1em}
section header {margin-left: -1em}

h2, h3, h4, h5, h6 { margin-bottom: .75em }
p {margin-top: .5em; margin-bottom: .5em}
p:first-child, ul, ol {margin-top: 0}
.todo dt:not(:first-child) {margin-top: .5em}
p, li, dd {max-width: 80ex}

figure {display: inline-block; margin: 1ex 1em;
        border: thin solid #f88; background-color: #eef; padding: 1ex}
figcaption + p { margin-top: 0 }
figure > p:last-child { margin-bottom:0 }
figure > * { margin-left: 1em }
figure > figcaption { margin-left: 0 }
figcaption { font-weight: bold }

div.example {display: inline-block; clear: both; margin-left: 1ex;
             border: thin solid #dfd; background-color: #f8f8f8; padding: 1ex}

div.note > *:first-child::before {content: "Note: "; display: inline; font-weight: bold}
div.note {display: inline-block; margin-left: 1ex; border: thin solid #8f8;
          padding: 1ex; background-color: #efe}

:target {background-color: #fed}
"""),
            E.script(dict(type='text/javascript',
                          src='https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js')),
            E.script(dict(type='text/javascript'), open('toc.js').read()),
            ),
        E.body(E.header(
                E.div({'class': 'docinfo'},
                      E.p('ISO/IEC JTC1 SC22 WG21 %s' %
                          indexpage.xpath('string(//document-number)')),
                      E.p('Date: ', E.time(dict(pubdate=''),
                                           datetime.date.today().isoformat())),
                      *render_flow(indexpage.xpath('//simplesect[@kind="author"]')[0])
                      ),
                E.h1(title)
                ),
               E.nav(dict(id='toc')),
               *render_flow(uniquechild(indexpage, 'detaileddescription'))
               )
        )
    remove_broken_links(result)
    merge_adjacent_dls(result)
    sys.stdout.buffer.write(etree.tostring(result, encoding='ascii', method='html',
                                           doctype='<!DOCTYPE html>'))

if __name__ == '__main__':
    main(sys.argv)
